package datastore.db;

import java.util.List;
import java.util.Map;

import datastore.api.DatastoreAPI;
import datastore.api.Entity;
import datastore.api.Reference;

public class Datastore {
    private static DatastoreAPI impl;

    //TODO: fix bootstrap, scan for entities, inject datastore
    public static void init(DatastoreAPI api, String appName) {
        impl = api;
        impl.init(appName);
    }

    /*
     * 
     * get(keys)
     * 
     * Gets the entity or entities for the given key or keys, of any Model.
     * 
     * Arguments:
     * 
     * keys A Key object or a list of Key objects.
     * 
     * If one Key is provided, the return value is an instance of the appropriate Model class, or None if no entity
     * exists with the given Key. If a list of Keys is provided, the return value is a corresponding list of model
     * instances, with None values when no entity exists for a corresponding Key.
     * 
     * See also Model.get().
     */
    @SuppressWarnings("unchecked")
    public static List<Model> get(Key ... keys) {
        return (List<Model>) impl.get(toReferences(keys));
    }

    /*
     * put(models)
     * 
     * Puts one or more model instances into the datastore.
     * 
     * Arguments:
     * 
     * models A model instance or a list of model instances to store.
     * 
     * If multiple model instances are given, they may be in more than one entity group. Within each entity group, all
     * entities belonging to that group will be written in a single transaction. See Keys and Entity Groups.
     * 
     * An exception will always be raised if any error occurs during the operation, even if some of the entities
     * actually were written. This may happen if the entities in the call span multiple entity groups. If the call
     * returns without raising an exception, then all of the entities were written successfully.
     * 
     * Returns the Key object (if one model instance is given) or a list of Key objects (if a list of instances is
     * given) that correspond with the stored model instances.
     */
    @SuppressWarnings("unchecked")
    public static Key put(Model model) {
        List<Key> list = (List<Key>) impl.put(toEntity(model));
        if (list == null || list.isEmpty()) {
            return null;
        }
        return list.get(0);
    }

    @SuppressWarnings("unchecked")
    public static List<Key> put(Model... models) {
        return (List<Key>) impl.put(toEntities(models));
    }

    /*
     * delete(models)
     * 
     * Deletes one or more model instances from the datastore.
     * 
     * Arguments:
     * 
     * models A model instance, a Key for an entity, or a list of model instances or keys of entities to delete.
     * 
     * As with Model.put(), if multiple keys are given, they may be in more than one entity group. Within each entity
     * group, all entities belonging to that group will be deleted in a single transaction. See Keys and Entity Groups.
     * 
     * An exception will always be raised if any error occurs during the operation, even if some of the entities
     * actually were deleted. This may happen if the keys in the call span multiple entity groups. If the call returns
     * without raising an exception, then all of the entities were deleted successfully. 
     */
    public static void delete(Model ... models) {
        impl.delete(toEntities(models));
    }

    /* run_in_transaction(function, args,kwargs)
     * 
     * Runs a function containing datastore updates in a single transaction. If any code raises an exception during the
     * transaction, all datastore updates made in the transaction are rolled back.
     * 
     * Arguments:
     * 
     * function The function to run in a datastore transaction.args Positional arguments to pass to the function.kwargs
     * Keyword arguments to pass to the function.
     * 
     * If the function returns a value, run_in_transaction() returns the value to the caller.
     * 
     * If the function raises an exception, the transaction is rolled back. If the function raises a Rollback exception,
     * the exception is not re-raised. For any other exception, the exception is re-raised to the caller.
     * 
     * The datastore uses optimistic locking and retries for transactions. If the transaction prepared by the function
     * cannot be committed, run_in_transaction() calls the function again, retrying the transaction up to 3 times. (To
     * use a different number of retries, use db.run_in_transaction_custom_retries().) Because the transaction function
     * may be called more than once for a single transaction, the function should not have side effects, including
     * modifications to arguments.
     * 
     * If the transaction cannot be committed, such as due to a high rate of contention, a TransactionFailedError is
     * raised.
     * 
     * For more information about transactions, see Transactions.
     * 
     * def decrement(key, amount=1): counter = db.get(key) counter.count -= amount if counter.count < 0: # don't let the
     * counter go negative raise db.Rollback() db.put(counter)
     * 
     * q = db.GqlQuery("SELECT * FROM Counter WHERE name = :1", "foo") counter = q.get()
     * db.run_in_transaction(decrement, counter.key(), amount=5)
     */
    public static Object runInTransaction(TransactionCallback transactionCallback) {
        return runInTransaction(0, transactionCallback);
    }
    
    /* run_in_transaction_custom_retries(retries, function,args,kwargs)
     * 
     * Runs a function containing datastore updates in a single transaction, retrying the transaction the given number
     * of times in the event of contention. If any code raises an exception during the transaction, all datastore
     * updates made in the transaction are rolled back.
     * 
     * Arguments:
     * 
     * retries The maximum number of times to call the transaction function in the event of contention in the entity
     * group (more than one user attempting to modify the group simultaneously). function The function to run in a
     * datastore transaction.args Positional arguments to pass to the function.kwargs Keyword arguments to pass to the
     * function.
     * 
     * Other than the ability to specify the number of retries, this function behaves identically to
     * db.run_in_transaction().
     */
    public static Object runInTransaction(int retries, TransactionCallback transactionCallback) {
        //TODO: implement transactions
        return transactionCallback.doInTransaction();
    }

    /**
     * Gets the model instance (or instances) for the given Key objects. The keys must represent entities of the model's
     * kind. If a provided key is not of the correct kind, a KindError is raised.
     * 
     * This method is similar to the db.get() function, with additional type checking.
     * 
     * @param keys
     *            A Key object or a list of Key objects. Can also be a string version of a Key object, or list of
     *            strings.
     */
    @SuppressWarnings("unchecked")
    public static <M extends Model> List<M> get(Class<M> modelClass, Key ... keys) {
        return (List<M>) impl.get(toReferences(keys));
    }
    
    @SuppressWarnings("unchecked")
    public static <M extends Model> M get(Class<M> modelClass, Key key) {
        List<M> models = (List<M>) impl.get(toReference(key));
        if (models == null || models.isEmpty()) {
            return null;
        }
        return models.get(0);
    }


    /**
     * Gets the model instance (or instances) for the given numeric ID (or IDs). If ids is a string representing one
     * name, then the method returns the model instance for the name, or None if the entity doesn't exist. If ids is a
     * list, the method returns a list of model instances, with a None value when no entity exists for a corresponding
     * Key.
     * 
     * @param ids
     *            A numeric entity ID, or a list of numeric entity IDs.
     * @param parent
     *            The parent entity for the requested entities, as a Model instance or Key instance, or None (the
     *            default) if the requested entities do not have a parent. Multiple entities requested by one call must
     *            all have the same parent.
     * @return
     */
    public static <M extends Model> List<M> getById(Class<M> modelClass, M parent, long ... ids) {
        return getById(modelClass, (parent != null)? parent.getKey() : null, ids);
    }
    
    @SuppressWarnings("unchecked")
    public static <M extends Model> List<M> getById(Class<M> modelClass, Key parentKey, long ... ids) {
        Key[] keys = new Key[ids.length];
        for (int i = 0; i < ids.length; i++) {
            keys[i] = Key.fromPath(parentKey, modelClass, ids[i]);
        }
        return (List<M>) impl.get(toReferences(keys));
    }

    public static <M extends Model> List<M> getById(Class<M> modelClass, long ... ids) {
        return getById(modelClass, (Key)null, ids);
    }

    public static <M extends Model> M getById(Class<M> modelClass, M parent, long id) {
        return getById(modelClass, (parent != null)? parent.getKey() : null, id);
    }
    
    @SuppressWarnings("unchecked")
    public static <M extends Model> M getById(Class<M> modelClass, Key parentKey, long id) {
        List<M> list = (List<M>) impl.get(toReference(Key.fromPath(modelClass, id)));
        if (list == null || list.isEmpty()) {
            return null;
        }
        return list.get(0);
    }
    
    public static <M extends Model> M getById(Class<M> modelClass, long id) {
        return getById(modelClass, (Key)null, id);
    }

    /**
     * Gets the model instance (or instances) for the given key name (or names). If key_names is a string representing
     * one name, then the method returns the model instance for the name, or None if the entity doesn't exist. If
     * key_names is a list, the method returns a list of model instances, with a None value when no entity exists for a
     * corresponding Key.
     * 
     * @param keyNames
     *            A key name, or a list of key names.
     * @param parent
     *            The parent entity for the requested entities, as a Model instance or Key instance, or None (the
     *            default) if the requested entities do not have a parent. Multiple entities requested by one call must
     *            all have the same parent.
     * @return
     */
    public static <M extends Model> M getByKeyName(Class<M> modelClass, String keyName) {
        throw new UnsupportedOperationException();
    }

    public static <M extends Model> List<M> getByKeyName(Class<M> modelClass, String ... keyNames) {
        throw new UnsupportedOperationException();
    }

    public static <M extends Model> List<M> getByKeyName(Class<M> modelClass, M parent, String ... keyNames) {
        throw new UnsupportedOperationException();
    }

    /*
     * Model.get_or_insert(key_name,kwds)
     * 
     * Get or create an entity of the model's kind with the given key name, using a single transaction. The transaction
     * ensures that if two users attempt to get-or-insert the entity with the given name simultaneously, then both users
     * will have a model instance that refers to the entity, regardless of which process created it.
     * 
     * Arguments:
     * 
     * key_name The name for the key of the entity
     * kwds Keyword arguments to pass to the model class if an instance with
     * the specified key name doesn't exist. The parent argument is required if the desired entity has a parent.
     * 
     * The method returns an instance of the model class that represents the requested entity, whether it existed or was
     * created by the method. As with all datastore operations, this method can raise a TransactionFailedError if the
     * transaction could not be completed. 
     */
    public static <M extends Model> M getOrInsert(Class<M> modelClass, String keyName) {
        throw new UnsupportedOperationException();
    }
     
    /*Model.all()
     * 
     * Returns a Query object that represents all entities for the kind corresponding to this model. Methods on the
     * Query object can apply filters and sort orders to the query before it is executed. See Query for more information
     */
    public static <M extends Model> Query<M> all(Class<M> modelClass) {
        return new Query<M>(modelClass);
    }

    /*
     * Model.gql(query_string,args,kwds)
     * 
     * Performs a GQL query over instances of this model.
     * 
     * Arguments:
     * 
     * query_string The part of the GQL query following SELECT FROM model (which is implied by using this class method).
     * args Positional parameter bindings, similar to the GqlQuery constructor.kw Keyword parameter bindings, similar to
     * the GqlQuery constructor.
     * 
     * s = Story.gql("WHERE title = :1", "Little Red Riding Hood")
     * 
     * s = Story.gql("WHERE title = :title", title="Little Red Riding Hood")
     * 
     * The return value is a GqlQuery object, which can be used to access the results. Model.kind() Returns the kind of
     * the model, usually the name of the Model subclass. Model.properties() Returns a dictionary of all of the
     * properties defined for this model class.
     */
    public static <M extends Model> GqlQuery<M> gql(Class<M> modelClass, String gql, Object ... args) {
        throw new UnsupportedOperationException();
    }
    
    public static <M extends Model> GqlQuery<M> gql(Class<M> modelClass, String gql, Map<String, Object> args) {
        throw new UnsupportedOperationException();
    }
    
    /////////////////////////////////////////////////////////////////////
    // package private helper methods for other db package classes
    /////////////////////////////////////////////////////////////////////
    @SuppressWarnings("unchecked")
    static <M extends Model> List<M> fetch(BaseQuery<M> query, int limit, int offset) {
        return (List<M>) impl.fetch(toQuery(query), limit, offset);
    }
    
    @SuppressWarnings("unchecked")
    static <M extends Model> Iterable<M> iterable(BaseQuery<M> query, int limit, int offset) {
        return (Iterable<M>) impl.iterable(toQuery(query), limit, offset);
    }
  
    static String getAppName() {
        return impl.getAppName();
    }

    /////////////////////////////////////////////////////////////////////
    // private methods
    /////////////////////////////////////////////////////////////////////
    
    private static datastore.api.Query toQuery(BaseQuery<?> base) {
        return new datastore.api.Query(base);
    }
    
    private static Reference[] toReferences(Key[] keys) {
        if (keys == null) {
            return new Reference[]{};
        }
        Reference[] refs = new Reference[keys.length];
        for (int i = 0; i < keys.length; i++) {
            refs[i] = toReference(keys[i]);
        }
        return refs;
    }

    private static Reference toReference(Key key) {
        return new Reference(key);
    }

    private static Entity[] toEntities(Model[] models) {
        if (models == null) {
            return new Entity[]{};
        }
        Entity[] entities = new Entity[models.length];
        for (int i = 0; i < models.length; i++) {
            entities[i] = toEntity(models[i]);
        }
        return entities;
    }

    private static Entity toEntity(Model model) {
        return new Entity(model);
    }

}
